using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Mapping.Permissions;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Models.Membership.Permissions;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Persistence.Mappers;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;

namespace Umbraco.Cms.Tests.Integration.ManagementApi.Factories;

[TestFixture]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
public class UserPresentationFactoryTests : UmbracoIntegrationTestWithContent
{
    public IUserPresentationFactory UserPresentationFactory => GetRequiredService<IUserPresentationFactory>();

    public IUserGroupService UserGroupService => GetRequiredService<IUserGroupService>();

    public IUserService UserService => GetRequiredService<IUserService>();

    public ILanguageService LanguageService => GetRequiredService<ILanguageService>();

    public IMediaService MediaService => GetRequiredService<IMediaService>();

    protected override void ConfigureTestServices(IServiceCollection services)
    {
        services.AddTransient<IUserPresentationFactory, UserPresentationFactory>();
        services.AddTransient<IUserGroupPresentationFactory, UserGroupPresentationFactory>();
        services.AddSingleton<IAbsoluteUrlBuilder, DefaultAbsoluteUrlBuilder>();
        services.AddSingleton<IUrlAssembler, DefaultUrlAssembler>();
        services.AddSingleton<IPasswordConfigurationPresentationFactory, PasswordConfigurationPresentationFactory>();
        services.AddSingleton<IPermissionPresentationFactory, PermissionPresentationFactory>();

        services.AddSingleton<IPermissionMapper, DocumentPermissionMapper>();
        services.AddSingleton<IPermissionPresentationMapper, DocumentPermissionMapper>();

        services.AddSingleton<IPermissionMapper, DocumentPropertyValuePermissionMapper>();
        services.AddSingleton<IPermissionPresentationMapper, DocumentPropertyValuePermissionMapper>();

        services.AddSingleton<IPermissionMapper, CustomPermissionMapper>();
        services.AddSingleton<IPermissionPresentationMapper, CustomPermissionMapper>();

    }

    [Test]
    public async Task Can_Create_Current_User_Response_Model()
    {
        var daLanguage = new LanguageBuilder()
            .WithCultureInfo("da-DK")
            .Build();
        await LanguageService.CreateAsync(daLanguage, Constants.Security.SuperUserKey);
        var enUsLanguage = await LanguageService.GetAsync("en-US");
        var daDkLanguage = await LanguageService.GetAsync("da-DK");

        var rootMediaFolder = MediaService.CreateMedia("Pictures Folder", Constants.System.Root, "Folder");
        MediaService.Save(rootMediaFolder);

        var groupOne = await CreateUserGroup(
            "Group One",
            "groupOne",
            [enUsLanguage.Id],
            [],
            [],
            rootMediaFolder.Id);
        var groupTwo = await CreateUserGroup(
            "Group Two",
            "groupTwo",
            [daDkLanguage.Id],
            [],
            [],
            rootMediaFolder.Id);

        var user = await CreateUser([groupOne.Key, groupTwo.Key]);

        var model = await UserPresentationFactory.CreateCurrentUserResponseModelAsync(user);

        Assert.AreEqual(user.Key, model.Id);
        Assert.AreEqual("test@test.com", model.Email);
        Assert.AreEqual("Test User", model.Name);
        Assert.AreEqual("test@test.com", model.UserName);
        Assert.AreEqual(2, model.UserGroupIds.Count);
        Assert.IsTrue(model.UserGroupIds.Select(x => x.Id).ContainsAll([groupOne.Key, groupTwo.Key]));
        Assert.IsFalse(model.HasAccessToAllLanguages);
        Assert.AreEqual(2, model.Languages.Count());
        Assert.IsTrue(model.Languages.ContainsAll(["en-US", "da-DK"]));
        Assert.IsTrue(model.HasDocumentRootAccess);
        Assert.AreEqual(0, model.DocumentStartNodeIds.Count);
        Assert.IsFalse(model.HasMediaRootAccess);
        Assert.AreEqual(1, model.MediaStartNodeIds.Count);
        Assert.AreEqual(rootMediaFolder.Key, model.MediaStartNodeIds.First().Id);
        Assert.IsFalse(model.HasAccessToSensitiveData);
    }

    [Test]
    public async Task Can_Create_Current_User_Response_Model_With_Aggregated_Document_Permissions()
    {
        var rootContentKey = Guid.Parse(TextpageKey);
        var subPageContentKey = Guid.Parse(SubPageKey);
        var subPage2ContentKey = Guid.Parse(SubPage2Key);

        var rootMediaFolder = MediaService.CreateMedia("Pictures Folder", Constants.System.Root, "Folder");
        MediaService.Save(rootMediaFolder);

        var groupOne = await CreateUserGroup(
            "Group One",
            "groupOne",
            [],
            ["A", "B", "C"],
            [
                new DocumentGranularPermission
                {
                    Key = rootContentKey,
                    Permission = "A",
                },
                new DocumentGranularPermission
                {
                    Key = rootContentKey,
                    Permission = "E",
                },
                new DocumentGranularPermission
                {
                    Key = subPageContentKey,
                    Permission = "F",
                },
                new DocumentGranularPermission
                {
                    Key = subPage2ContentKey,
                    Permission = "F",
                }
            ],
            rootMediaFolder.Id);
        var groupTwo = await CreateUserGroup(
            "Group Two",
            "groupTwo",
            [],
            ["A", "B", "D"],
            [
                new DocumentGranularPermission
                {
                    Key = subPage2ContentKey,
                    Permission = "G",
                },
                new DocumentGranularPermission
                {
                    Key = subPage2ContentKey,
                    Permission = "H",
                }
            ],
            rootMediaFolder.Id);

        var user = await CreateUser([groupOne.Key, groupTwo.Key]);

        var model = await UserPresentationFactory.CreateCurrentUserResponseModelAsync(user);

        Assert.AreEqual(4, model.FallbackPermissions.Count);
        Assert.IsTrue(model.FallbackPermissions.ContainsAll(["A", "B", "C", "D"]));

        // When aggregated, we expect one permission per document (we have several granular permissions assigned, for three unique documents).
        Assert.AreEqual(3, model.Permissions.Count);

        // User has two user groups, one of which provides granular permissions for the root content item.
        // As such we expect the aggregated permissions to be the union of the specific permissions coming from the user group with them assigned to the document,
        // and the fallback permissions from the other.
        var rootContentPermissions = model.Permissions.Cast<DocumentPermissionPresentationModel>().Single(x => x.Document.Id == rootContentKey);
        Assert.AreEqual(4, rootContentPermissions.Verbs.Count);
        Assert.IsTrue(rootContentPermissions.Verbs.ContainsAll(["A", "B", "D", "E"]));

        // The sub-page and it's parent have specific granular permissions from one user group.
        // So we expect the aggregated permissions to include those from the sub-page and the other user's groups fallback permissions.
        var subPageContentPermissions = model.Permissions.Cast<DocumentPermissionPresentationModel>().Single(x => x.Document.Id == subPageContentKey);
        Assert.AreEqual(4, subPageContentPermissions.Verbs.Count);
        Assert.IsTrue(subPageContentPermissions.Verbs.ContainsAll(["A", "B", "D", "F"]));

        // Both user groups provide granular permissions for the second sub-page content item.
        // Here we expect the aggregated permissions to be the union of the granular permissions on the document from both user groups.
        var subPage2ContentPermissions = model.Permissions.Cast<DocumentPermissionPresentationModel>().Single(x => x.Document.Id == subPage2ContentKey);
        Assert.AreEqual(3, subPage2ContentPermissions.Verbs.Count);
        Assert.IsTrue(subPage2ContentPermissions.Verbs.ContainsAll(["F", "G", "H"]));
    }

    [Test]
    public async Task Can_Create_Current_User_Response_Model_With_Aggregated_Document_Property_Value_Permissions()
    {
        var propertyTypeKey = Guid.NewGuid();
        var propertyTypeKey2 = Guid.NewGuid();
        var groupOne = await CreateUserGroup(
            "Group One",
            "groupOne",
            [],
            [],
            [
                new DocumentGranularPermission
                {
                    Key = Guid.Parse(TextpageKey),
                    Permission = "A",
                },
                new DocumentPropertyValueGranularPermission
                {
                    Key = ContentType.Key,
                    Permission = $"{propertyTypeKey}|C",
                },
                new DocumentPropertyValueGranularPermission
                {
                    Key = ContentType.Key,
                    Permission = $"{propertyTypeKey2}|D",
                },
            ],
            Constants.System.Root);
        var groupTwo = await CreateUserGroup(
            "Group Two",
            "groupTwo",
            [],
            [],
            [
                new DocumentPropertyValueGranularPermission
                {
                    Key = ContentType.Key,
                    Permission = $"{propertyTypeKey}|B",
                },
            ],
            Constants.System.Root);
        var user = await CreateUser([groupOne.Key, groupTwo.Key]);

        var model = await UserPresentationFactory.CreateCurrentUserResponseModelAsync(user);
        Assert.AreEqual(3, model.Permissions.Count);

        var documentPermissions = model.Permissions
            .Where(x => x is DocumentPermissionPresentationModel)
            .Cast<DocumentPermissionPresentationModel>()
            .Single(x => x.Document.Id == Guid.Parse(TextpageKey));
        Assert.AreEqual(1, documentPermissions.Verbs.Count);
        Assert.IsTrue(documentPermissions.Verbs.ContainsAll(["A"]));

        var documentPropertyValuePermissions = model.Permissions
            .Where(x => x is DocumentPropertyValuePermissionPresentationModel)
            .Cast<DocumentPropertyValuePermissionPresentationModel>()
            .Where(x => x.DocumentType.Id == ContentType.Key);
        Assert.AreEqual(2, documentPropertyValuePermissions.Count());

        var propertyTypePermission1 = documentPropertyValuePermissions
            .Single(x => x.PropertyType.Id == propertyTypeKey);
        Assert.AreEqual(2, propertyTypePermission1.Verbs.Count);
        Assert.IsTrue(propertyTypePermission1.Verbs.ContainsAll(["B", "C"]));

        var propertyTypePermission2 = documentPropertyValuePermissions
            .Single(x => x.PropertyType.Id == propertyTypeKey2);
        Assert.AreEqual(1, propertyTypePermission2.Verbs.Count);
        Assert.IsTrue(propertyTypePermission2.Verbs.ContainsAll(["D"]));
    }

    [Test]
    public async Task Can_Create_Current_User_Response_Model_With_Aggregated_Custom_Permissions()
    {
        var key1 = Guid.NewGuid();
        var key2 = Guid.NewGuid();
        var groupOne = await CreateUserGroup(
            "Group One",
            "groupOne",
            [],
            [],
            [
                new CustomGranularPermission
                {
                    Permission = $"{key1}|A",
                },
                new CustomGranularPermission
                {
                    Permission = $"{key1}|B",
                },
                new CustomGranularPermission
                {
                    Permission = $"{key2}|C",
                }
            ],
            Constants.System.Root);
        var groupTwo = await CreateUserGroup(
            "Group Two",
            "groupTwo",
            [],
            [],
            [
                new CustomGranularPermission
                {
                    Permission = $"{key1}|A",
                },
                new CustomGranularPermission
                {
                    Permission = $"{key2}|B",
                },
            ],
            Constants.System.Root);
        var user = await CreateUser([groupOne.Key, groupTwo.Key]);

        var model = await UserPresentationFactory.CreateCurrentUserResponseModelAsync(user);
        Assert.AreEqual(2, model.Permissions.Count);

        var customPermissions = model.Permissions
            .Where(x => x is CustomPermissionPresentationModel)
            .Cast<CustomPermissionPresentationModel>();
        Assert.AreEqual(2, customPermissions.Count());

        var customPermission1 = customPermissions
            .Single(x => x.Key == key1);
        Assert.AreEqual(2, customPermission1.Verbs.Count);
        Assert.IsTrue(customPermission1.Verbs.ContainsAll(["A", "B"]));

        var customPermission2 = customPermissions
            .Single(x => x.Key == key2);
        Assert.AreEqual(2, customPermission2.Verbs.Count);
        Assert.IsTrue(customPermission2.Verbs.ContainsAll(["B", "C"]));
    }

    private class CustomGranularPermission : IGranularPermission
    {
        public const string ContextType = "Custom";

        public string Context => ContextType;

        public required string Permission { get; set; }

        protected bool Equals(CustomGranularPermission other) => Permission == other.Permission;

        public override bool Equals(object? obj)
        {
            if (ReferenceEquals(null, obj))
            {
                return false;
            }

            if (ReferenceEquals(this, obj))
            {
                return true;
            }

            if (obj.GetType() != GetType())
            {
                return false;
            }

            return Equals((CustomGranularPermission)obj);
        }

        public override int GetHashCode() => HashCode.Combine(Permission);
    }

    private class CustomPermissionPresentationModel : IPermissionPresentationModel
    {
        public required ISet<string> Verbs { get; set; }

        public required Guid Key { get; set; }
    }

    private class CustomPermissionMapper : IPermissionMapper, IPermissionPresentationMapper
    {
        public string Context => CustomGranularPermission.ContextType;

        public Type PresentationModelToHandle => typeof(CustomPermissionPresentationModel);

        public IGranularPermission MapFromDto(UserGroup2GranularPermissionDto dto)
        {
            return new CustomGranularPermission
            {
                Permission = dto.Permission,
            };
        }

        public IEnumerable<IPermissionPresentationModel> MapManyAsync(IEnumerable<IGranularPermission> granularPermissions)
            => granularPermissions
                .Where(x => x is CustomGranularPermission)
                .Cast<CustomGranularPermission>()
                .Select(x => new CustomPermissionPresentationModel
                {
                    Key = Guid.Parse(x.Permission.Split('|')[0]),
                    Verbs = new HashSet<string> { x.Permission.Split('|')[1] },
                });

        public IEnumerable<IGranularPermission> MapToGranularPermissions(IPermissionPresentationModel permissionViewModel)
        {
            if (permissionViewModel is not CustomPermissionPresentationModel customPermissionPresentationModel)
            {
                yield break;
            }

            foreach (var verb in customPermissionPresentationModel.Verbs.Distinct().DefaultIfEmpty(string.Empty))
            {
                yield return new CustomGranularPermission
                {
                    Permission = customPermissionPresentationModel.Key + "|" + verb,
                };
            }
        }

        public IEnumerable<IPermissionPresentationModel> AggregatePresentationModels(IUser user, IEnumerable<IPermissionPresentationModel> models)
        {
            IEnumerable<(Guid Key, ISet<string> Verbs)> groupedModels = models
                .Cast<CustomPermissionPresentationModel>()
                .GroupBy(x => x.Key)
                .Select(x => (x.Key, (ISet<string>)x.SelectMany(y => y.Verbs).Distinct().ToHashSet()));

            foreach ((Guid key, ISet<string> verbs) in groupedModels)
            {
                yield return new CustomPermissionPresentationModel
                {
                    Key = key,
                    Verbs = verbs,
                };
            }
        }
    }

    private async Task<IUserGroup> CreateUserGroup(
        string name,
        string alias,
        int[] allowedLanguages,
        string[] permissions,
        IGranularPermission[] granularPermissions,
        int startMediaId)
    {
        var userGroup = new UserGroupBuilder()
            .WithName(name)
            .WithAlias(alias)
            .WithAllowedLanguages(allowedLanguages)
            .WithStartMediaId(startMediaId)
            .WithPermissions(permissions.ToHashSet())
            .WithGranularPermissions(granularPermissions)
            .Build();
        var createUserGroupResult = await UserGroupService.CreateAsync(userGroup, Constants.Security.SuperUserKey);
        Assert.IsTrue(createUserGroupResult.Success);
        return userGroup;
    }

    private async Task<IUser> CreateUser(Guid[] userGroupKeys)
    {
        var createUserAttempt = await UserService.CreateAsync(Constants.Security.SuperUserKey, new UserCreateModel
        {
            Email = "test@test.com",
            Name = "Test User",
            UserName = "test@test.com",
            UserGroupKeys = userGroupKeys.ToHashSet(),
        });
        Assert.IsTrue(createUserAttempt.Success);

        return await UserService.GetAsync(createUserAttempt.Result.CreatedUser.Key);
    }
}
